﻿using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Audio;
using System.Diagnostics;

namespace TestGame.GameLib {
    /// <summary>
    /// The enum used to return the result of any probing 
    /// into the BlobArray.
    /// </summary>
    public enum GridResult {
        /// <summary>
        /// The current position is occuped by another Blob.
        /// </summary>
        OCCUPIED,

        /// <summary>
        /// The current position is out-of-bounds for the array.
        /// </summary>
        OUT_OF_BOUNDS,

        /// <summary>
        /// The current position is unoccuped.
        /// </summary>
        UNOCCUPIED
    }

    /// <summary>
    /// Represents an array of blobs that updates and removes Blobs as 
    /// necessary. 
    /// </summary>
    class BlobArray {
        #region Fields

        /// <summary>
        /// A simple two dimmensional array to kept track of the Blobs
        /// </summary>
        Blob[,] blobArray;

        /// <summary>
        /// A boolean to keep track if all blobs are resolved. Resolved means that the blobs are
        /// in their final positions, and no Blobs are waiting to be removed.
        /// </summary>
        bool isResolved;

        /// <summary>
        /// A soundeffect to be played when a Blob Combo is found and removed.
        /// </summary>
        static SoundEffect blobClearSFX;

        /// <summary>
        /// The texture for the Grid's backdrop
        /// </summary>
        static Texture2D gridTexture;

        /// <summary>
        /// A constant representing an empty spot in the Blob Array.
        /// </summary>
        const byte EmptySpace = Byte.MaxValue;

        #endregion

        #region Properties

        /// <summary>
        /// The Width of the BlobArray
        /// </summary>
        public int Width {
            get { return blobArray.GetLength(0); }
        }

        /// <summary>
        /// The Height of the BlobArray
        /// </summary>
        public int Height {
            get { return blobArray.GetLength(1); }
        }

        /// <summary>
        /// Determines if the BlobArray has all Blob positions currently resolved.
        /// </summary>
        public bool IsResolved {
            get { return isResolved; }
        }

        /// <summary>
        /// The additional height needed to nicely place things.
        /// </summary>
        public const int BufferHeight = 10;

        #endregion

        #region Initialization

        /// <summary>
        /// Creates a new BlobArray given the width and height.
        /// </summary>
        /// <param name="width"></param>
        /// <param name="height"></param>
        public BlobArray(int width, int height) {
            isResolved = true;
            blobArray = new Blob[width, height + BufferHeight];
        }

        /// <summary>
        /// Loads all content needed for drawing or sound calls.
        /// </summary>
        /// <param name="content"></param>
        public static void LoadContent(ContentManager content) {
            blobClearSFX = content.Load<SoundEffect>("blip-clear");
            gridTexture = content.Load<Texture2D>("grid");
        }

        #endregion

        #region Update Method

        /// <summary>
        /// Updates all Blobs in the array.
        /// </summary>
        /// <param name="gameTime"></param>
        public void Update(GameTime gameTime) {
            /* A simple loop to update and Blob's in the Grid.
             * Blob's Update() is used to decrement the Timer once they are determined to be 
             * removed. */
            foreach (Blob blob in blobArray) {
                if (blob != null) {
                    blob.Update(gameTime);
                }
            }
        }

        #endregion

        #region Draw Method

        /// <summary>
        /// Draws all current Blobs in the array, as well as any blobs 
        /// graphics. The drawn position is determined largely by the 
        /// GraphicalOffset property. Changing this value allows to change 
        /// where the Blobs and Grid texture are drawn.
        /// </summary>
        /// <param name="spriteBatch"></param>
        /// <param name="content"></param>
        public void Draw(SpriteBatch spriteBatch, ContentManager content) {
            spriteBatch.Draw(gridTexture, PlayingArea.GraphicOffset, Color.White);

            foreach (Blob blob in blobArray) {
                if (blob != null) {
                    blob.Draw(spriteBatch, content);
                }
            }
        }

        #endregion

        #region Grid Probing Methods

        /// <summary>
        /// Adds the given Blob to the array, according to its rounded 
        /// position. If there is a Blob already in its spot, an 
        /// InvalidOperationException is raised.
        /// </summary>
        /// <param name="target"></param>
        public void AddToPlayingArea(Blob target) {
            target.RoundPosition();

            if (blobArray[target.X, target.Y] == null) {
                blobArray[target.X, target.Y] = target;
                isResolved = false;
            }
            else {
                throw new InvalidOperationException("Spot is already occupied");
            }
        }

        /// <summary>
        /// Finds the current status for the specified position.
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <returns>Occupied, OutOfBounds, or Unoccupied depending on the 
        /// result.</returns>
        public GridResult GetVacancyFor(int x, int y) {
            GridResult result = GridResult.OCCUPIED;

            if (x < 0 || x >= blobArray.GetLength(0)) {
                result = GridResult.OUT_OF_BOUNDS;
            }
            else if (y < 0 || y >= blobArray.GetLength(1)) {
                result = GridResult.OUT_OF_BOUNDS;
            }
            else if (blobArray[x, y] == null) {
                result = GridResult.UNOCCUPIED;
            }

            return result;
        }

        /// <summary>
        /// Gets all non-null neighbors for the specified Blob.
        /// </summary>
        /// <param name="target"></param>
        /// <returns></returns>
        public Blob[] GetNeighboringBlobs(Blob target) {
            List<Blob> neighbors = new List<Blob>();
            int x = target.X;
            int y = target.Y;

            // Left Neighbor
            if (GetVacancyFor(x - 1, y) == GridResult.OCCUPIED) {
                neighbors.Add(blobArray[x - 1, y]);
            }

            // Right Neighbor
            if (GetVacancyFor(x + 1, y) == GridResult.OCCUPIED) {
                neighbors.Add(blobArray[x + 1, y]);
            }

            // Top Neighbor
            if (GetVacancyFor(x, y - 1) == GridResult.OCCUPIED) {
                neighbors.Add(blobArray[x, y - 1]);
            }

            // Bottom Neighbor
            if (GetVacancyFor(x, y + 1) == GridResult.OCCUPIED) {
                neighbors.Add(blobArray[x, y + 1]);
            }

            return neighbors.ToArray();
        }

        /// <summary>
        /// Checks all Blobs in the array to see if any have the removal flag
        /// set, but are not ready to be removed. If they are ready to remove,
        /// the Method removes them and continues searching.
        /// </summary>
        /// <returns></returns>
        public bool AnyRemovableBlobs() {
            bool anyBlobsWaiting = false;

            for (int x = 0; x < blobArray.GetLength(0); x++) {
                for (int y = 0; y < blobArray.GetLength(1); y++) {
                    Blob blob = blobArray[x, y];

                    if (blob != null) {
                        if (blob.RemovalFlag) {
                            if (blob.IsDeathTimerOver()) {
                                blobArray[x, y] = null;
                            }
                            else {
                                anyBlobsWaiting = true;
                            }
                        }
                    }
                }
            }

            return anyBlobsWaiting;
        }

        #endregion

        #region Grid Resolution Methods

        public int Resolve() {
            int totalBlobsRemoved = 0;

            /* We first attempt to resolve the Blob positions first, which will only be successful 
             * if there are no Blobs needing to be moved or are currently moving. */
            isResolved = AttemptToResolvePositions();

            /* If we are successful in resolving the Blob positions, we then need to check if any
             * Blobs need to be removed, but first we must count the connected Blobs */
            if (isResolved) {
                foreach (Blob blob in blobArray) {
                    /* IsUpdated is a simple check to prevent recounting 
                     * of already counted blobs.*/
                    if (blob != null && !blob.IsUpdated) {
                        /* CheckConnectedBlobs will return the amount of 
                         * removed Blobs. If any blobs are removed, we must 
                         * take a step backEntry because the Grid is unresolved! */
                        int blobsRemoved = CheckConnectedBlobs(blob);

                        if (blobsRemoved > 0) {
                            Vector2 starPos = Blob.CalculateDrawPosition(blob.Position);
                            ParticleSystem.AddStandardStarParticles(starPos);

                            isResolved = false;
                            totalBlobsRemoved += blobsRemoved;
                        }
                    }
                }
            }

            // If any Blobs are removed, play a SFX for them.
            if (totalBlobsRemoved > 0) {
                blobClearSFX.Play();
            }

            return totalBlobsRemoved;
        }

        /// <summary>
        /// Checks for all connected Blobs in respect to the given Blob. If 
        /// the connected blob count passes the threshold, all the Blobs are
        /// flagged for removal.
        /// </summary>
        /// <param name="origin"></param>
        /// <returns>The amount of Blobs that were flagged for removal.</returns>
        private int CheckConnectedBlobs(Blob origin) {
            List<Blob> connected = new List<Blob>();
            List<Blob> toProcess = new List<Blob>(new Blob[] { origin });
            byte connectedCount = 0;
            int removedCount = 0;

            // If the Blob is gray, we don't want to count them, so just return.
            if (origin.Type == Blob.Gray) {
                return 0;
            }

            /* The basis of this search, is to find all matching neighbors, then use those
             * matching neighbors to find their matching neighbors, until there are no more
             * new matches found. Then the total connected blobs is the length of the array. */
            for (int i = 0; i < toProcess.Count; i++) {
                Blob toSearch = toProcess[i];

                if (toSearch.Type == origin.Type) {
                    Blob[] neighbors = GetNeighboringBlobs(toSearch);

                    foreach (Blob neighbor in neighbors) {
                        /* Minor optimization here, is to eliminate by Blob type first, then to do 
                         * a list iterative check so we don't do unnecessary searches. */
                        if (neighbor.Type == origin.Type && !toProcess.Contains(neighbor)) {
                            toProcess.Add(neighbor);
                        }
                        else if (neighbor.Type == Blob.Gray) {
                            connected.Add(neighbor);
                        }
                    }

                    connectedCount++;
                    connected.Add(toSearch);
                }
            }

            /* After the search is done and we found all connected Blobs, we check to see if
             * the connected Blobs are to be flaged for removal. */
            if (connectedCount < GameRules.ConnectedThreshHold) {
                foreach (Blob connectedBlob in connected) {
                    if (connectedBlob.Type != Blob.Gray) {
                        // This also sets IsUpdated to true.
                        connectedBlob.Connected = connectedCount;
                    }
                }
            }
            else {
                removedCount = connectedCount;

                foreach (Blob toRemove in connected) {
                    toRemove.Connected = connectedCount;
                    toRemove.RemovalFlag = true;
                }
            }

            return removedCount;
        }

        /// <summary>
        /// Attempts to resolve all the Blobs in the array. If the resolve is
        /// unsuccessful, then more calls to this Method are required.
        /// </summary>
        /// <returns>
        /// True if the resolve was successful, false if not.
        /// </returns>
        private bool AttemptToResolvePositions() {
            bool wasSuccessful = true;

            for (int x = 0; x < blobArray.GetLength(0); x++) {
                /* We start from the bottom first, to resolve lower Blobs so higher Blobs can
                 * be moved more efficently. */
                for (int y = blobArray.GetLength(1) - 1; y >= 0; y--) {
                    Blob blob = blobArray[x, y];

                    if (blob != null) {
                        /* If the Blob is not moving, then we need to check if it SHOULD be 
                         * moving. If it is moving or should be, we cannot state that the Grid is 
                         * resolved just yet. */
                        if (CheckForMovement(blob)) {
                            wasSuccessful = false;
                        }

                        /* This checks to see if the Blob is "moving". This flag is weird in the 
                         * sense that if a Blob is falling, it is preventing the Blobs from above it
                         * from moving down with it, causing a "traffic jam" of Blobs. To make a 
                         * smooth movement down, we find the lead Blob, which is flagged by this
                         * boolean, and move all others above it, down with it. */
                        if (blob.IsLowestBlob) {
                            for (int newY = y - 1; newY >= 0; newY--) {
                                Blob aboveBlob = blobArray[x, newY];

                                if (aboveBlob != null) {
                                    ShiftBlobDown(aboveBlob);
                                }
                            }
                        }
                    }
                }
            }

            return wasSuccessful;
        }

        /// <summary>
        /// Checks to see if the given Blob needs to be moved. 
        /// If the Blob does need to be moved, it flags it as a Low Blob, 
        /// which will attach all connected blobs above it, regardless of type,
        /// to fall with it.
        /// </summary>
        /// <param name="blob">The blob to be checked</param>
        /// <returns>True if there is movement, false is there is none</returns>
        private bool CheckForMovement(Blob blob) {
            bool anyMovement = false;

            /* Check the array position below the current Blob. If nothing is below 
             * it, start to move the blob down. If the Blob has movement, the Grid
             * is not yet resolved, so this is an unsuccessful resolve. We also
             * set IsUpdated to false because since the blob will have a new spot 
             * in the blobs, it will have different Blob connections. */
            if (GetVacancyFor(blob.X, blob.Y + 1) == GridResult.UNOCCUPIED) {
                ShiftBlobDown(blob);
                blob.IsLowestBlob = true;
                anyMovement = true;
            }
            else {
                blob.IsLowestBlob = false;
            }

            return anyMovement;
        }

        /// <summary>
        /// Moves the given Blob down in the blobs. Updates the Array if the 
        /// shift moves the Blob into a new index.
        /// </summary>
        /// <param name="toMove">The blob to move</param>
        private void ShiftBlobDown(Blob toMove) {
            int oldY = toMove.Y;

            // A simple shift down
            toMove.Position.Y += 0.2f;

            // Set the Blob to need an update
            toMove.IsUpdated = false;

            /* If the slow increment has pushed it past its old bounds into a new
             * array position, update the array to reflect this. While
             * technically this makes the array unresolved, we already flagged
             * the array as unresolved, so we don't care. */
            if (oldY != toMove.Y) {
                blobArray[toMove.X, oldY] = null;
                AddToPlayingArea(toMove);

                FlagLeftRightBlobs(toMove.X, oldY);
                FlagLeftRightBlobs(toMove.X, toMove.Y);
            }
        }

        /// <summary>
        /// Flags the left and right blobs to be updated. Used when a Blob is 
        /// falling past Blobs.
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        private void FlagLeftRightBlobs(int x, int y) {
            // Right neighbor
            if (GetVacancyFor(x + 1, y) == GridResult.OCCUPIED) {
                blobArray[x + 1, y].IsUpdated = false;
            }

            // Left neighbor
            if (GetVacancyFor(x - 1, y) == GridResult.OCCUPIED) {
                blobArray[x - 1, y].IsUpdated = false;
            }
        }

        #endregion

        #region Network Sync
        
        /// <summary>
        /// Using the given byte array, the game will update every Blob in the
        /// array. 
        /// </summary>
        /// <param name="data"></param>
        public void ImportData(byte[] data) {
            int ndx = 0;

            for (int x = 0; x < blobArray.GetLength(0); x++) {
                for (int y = 0; y < blobArray.GetLength(1); y++) {
                    if (data[ndx] == EmptySpace) {
                        blobArray[x, y] = null;
                    }
                    else {
                        blobArray[x, y] = new Blob(data[ndx], new Vector2(x, y));
                    }

                    ndx++;
                }
            }
        }

        /// <summary>
        /// Generates a byte array that can be sent to another Player and then 
        /// update their copy of the BlobArray.
        /// </summary>
        /// <returns></returns>
        public byte[] ExportData() {
            byte[] data = new byte[blobArray.GetLength(0) * blobArray.GetLength(1)];
            int ndx = 0;

            for (byte x = 0; x < blobArray.GetLength(0); x++) {
                for (byte y = 0; y < blobArray.GetLength(1); y++) {
                    Blob current = blobArray[x, y];

                    if (current == null) {
                        data[ndx] = EmptySpace;
                    }
                    else {
                        data[ndx] = current.Type;
                    }

                    ndx++;
                }
            }

            return data;
        }

        #endregion
    }
}
